[FC-0099] refactor: use decorator to manage policy lifecycle for low-level APIs#86
[FC-0099] refactor: use decorator to manage policy lifecycle for low-level APIs#86mariajgrimaldi wants to merge 9 commits intomainfrom
Conversation
|
Thanks for the pull request, @mariajgrimaldi! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
e2e50de to
e1ccc1b
Compare
| @@ -0,0 +1,377 @@ | |||
| """Test cases for decorator functions. | |||
There was a problem hiding this comment.
TODO: need to add tests for action grouping at least for enforcement.
e1ccc1b to
09f5f79
Compare
167f4fd to
3dca2a2
Compare
| from openedx_authz.engine.filter import Filter | ||
|
|
||
|
|
||
| def manage_policy_lifecycle(filter_on: str = ""): |
There was a problem hiding this comment.
nit: You can probably just annotate this return as Any from typing.
| Filter: A Filter object populated with relevant filter values. | ||
| """ | ||
| # Fallback to no filtering in case of misbehavior | ||
| if settings.ALLOW_FILTERED_POLICY_LOADING is False: |
There was a problem hiding this comment.
Are we planning on getting this working for Ulmo? If not, I would rather temporarily remove the code we're not using than have code that we don't trust just disabled via a flag.
There was a problem hiding this comment.
I agree! I can remove it and save it in another branch. My initial idea was to start by loading the entire policy and then address any issues with this approach retroactively. I did try adding support for filtered loading, but I ran into some issues while finishing the implementation and adding tests. As I mentioned here: https://github.com/openedx/openedx-authz/pull/86/files#diff-a6773904192669f0814472909dcec3567d5a893f6a97e6b8682cdd2ba8bcb55dR40-R55, in my opinion, making filtered loading consistent would probably require a different model, which isn't a priority right now. We could revisit it later if we notice resource consumption issues.
BryanttV
left a comment
There was a problem hiding this comment.
It looks great, I just have a few comments. I'll be testing these changes more thoroughly in my local environment.
|
|
||
| NAMESPACE: ClassVar[str] = "sc" | ||
| POLICY_POSITION = 2 # Position of scope in Casbin policy rules (p = sub, act, obj) | ||
| GROUPING_POLICY_POSITION = 2 # Position of scope in Casbin grouping policy rules (g = sub, role, scope) |
| Returns: | ||
| str: The policy template string. | ||
| """ | ||
| return f"{self.NAMESPACE}{self.SEPARATOR}*" |
There was a problem hiding this comment.
Could we use the GENERIC_SCOPE_WILDCARD variable instead of the * literal character?
| raise NotImplementedError("Subclasses must implement exists method.") | ||
|
|
||
| @property | ||
| def policy_template(self) -> str: |
There was a problem hiding this comment.
Could we add examples in the docstring? I think they are very useful for understanding.
| # Without filtering (loads all policies): | ||
| @manage_policy_lifecycle() | ||
| def get_all_roles(): | ||
| return enforcer.get_all_roles() | ||
|
|
||
| # With scope filtering (loads only relevant policies): | ||
| @manage_policy_lifecycle(filter_on="scope") | ||
| def get_roles_in_scope(scope: ScopeData): | ||
| return enforcer.get_filtered_roles(scope.namespaced_key) |
There was a problem hiding this comment.
To highlight the code:
| # Without filtering (loads all policies): | |
| @manage_policy_lifecycle() | |
| def get_all_roles(): | |
| return enforcer.get_all_roles() | |
| # With scope filtering (loads only relevant policies): | |
| @manage_policy_lifecycle(filter_on="scope") | |
| def get_roles_in_scope(scope: ScopeData): | |
| return enforcer.get_filtered_roles(scope.namespaced_key) | |
| - Without filtering (loads all policies):: | |
| @manage_policy_lifecycle() | |
| def get_all_roles(): | |
| return enforcer.get_all_roles() | |
| - With scope filtering (loads only relevant policies):: | |
| @manage_policy_lifecycle(filter_on="scope") | |
| def get_roles_in_scope(scope: ScopeData): | |
| return enforcer.get_filtered_roles(scope.namespaced_key) |
|
|
||
|
|
||
| @manage_policy_lifecycle(filter_on="scope") | ||
| def is_subject_allowed( |
There was a problem hiding this comment.
Please, remove the enforcer.load_policy()
| @manage_policy_lifecycle() | ||
| def get_permissions_for_single_role( |
There was a problem hiding this comment.
I'm concerned about having to load the policy twice, since this function is called internally by other functions here.
BryanttV
left a comment
There was a problem hiding this comment.
I have already tested the changes using the REST API with different users, and it loads the policies correctly. Thanks!
| return [get_permission_from_policy(action) for action in actions] | ||
|
|
||
|
|
||
| @manage_policy_lifecycle(filter_on="scope") |
There was a problem hiding this comment.
| @manage_policy_lifecycle(filter_on="scope") | |
| @manage_policy_lifecycle() |
There was a problem hiding this comment.
Please add the decorator to the get_subjects_for_role function
Here are my assumptions, please correct me if I'm wrong:
I think I've come around to the sync'd enforcer with a short timeout of like 5-10 seconds for the following reasons:
Compared to these, a few seconds of staleness seems to be the lesser evil. It is worth noting that it would still be 1 enforcer per process (by default Tutor runs 2 processes per container) so on a large instance with, say 15 lms/cms containers and 5 lms-worker/cms-worker containers running (each is only 1 process). So with 35 distinct Python processes it would still constantly hit the database several times a second at a 10 second cadence (though in most cases it would probably be cached). As such we should definitely make this configurable if we go this way. Thoughts for the future:
|
|
If approved, this PR should be closed in favor of #103 |
@mariajgrimaldi I approved on that one, but would appreciate your review there too |
|
This PR will be closed in favor of #103, other efforts to use load_filtered_policy will be addressed as part of a different effort which could reuse this initial approach. |
Description
Implement a decorator to handle the policy lifecycle before and after calling any public API method. The goal is to free clients from manually loading and clearing the enforcer, ensuring each operation uses the latest policies from the datastore.
The decorator should:
The main concern with this approach (and the previous one, though it's clearer here) is that we have a single enforcer per process. If two requests run in parallel and one finishes first, clearing the enforcer could affect the other request mid-operation.
Here's what I've been thinking:
What do you think?
Casbin references related to this topic: